Completed
Push — master ( ac4e15...137431 )
by
unknown
01:37
created

Link.js ➔ ... ➔ ???   A

Complexity

Conditions 1
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
c 2
b 2
f 0
nc 2
dl 0
loc 4
rs 10
cc 1
nop 2
1
import React, { PropTypes } from 'react';
2
import BaseRouterComponent from './Base';
3
4
import shallowCompare from 'react-addons-shallow-compare';
5
6
import { parse, format } from 'url';
7
import qs from 'query-string';
8
import * as actions from '../actions';
9
import {
10
    __DEV__,
11
    LINK_MATCH_EXACT,
12
    LINK_MATCH_PARTIAL,
13
    LINK_DEFAULT_METHOD,
14
    LINK_CLASSNAME,
15
    LINK_ACTIVE_CLASSNAME
16
} from '../constants';
17
18
19
const compareQueryItems = (linkQueryItem, routeQueryItem) => {
20
21
    linkQueryItem = [].concat(linkQueryItem);
22
    routeQueryItem = [].concat(routeQueryItem);
23
24
    return linkQueryItem.reduce((result, linkQuerySubItem) => {
25
        result = result && routeQueryItem.includes(linkQuerySubItem.toString());
26
        return result;
27
    }, true);
28
29
};
30
31
class Link extends BaseRouterComponent {
32
33
    constructor(props, context) {
34
35
        super(props, context);
36
37
        this.handleClick = this.handleClick.bind(this);
38
39
        this.href = this.getHref(props);
40
        this.state = {
41
            isActive: false
42
        };
43
    }
44
45
    componentWillReceiveProps(newProps) {
46
47
        if (this.props.to !== newProps.to) {
48
            this.href = this.getHref(newProps);
49
        }
50
    }
51
52
    shouldComponentUpdate(props, state) {
53
54
        return shallowCompare(this, props, state);
55
    }
56
57
    initiateLocationChange(e) {
58
        const { target } = this.props;
59
60
        if (!target && !this.href.protocol) {
61
            e.preventDefault();
62
            this.locationChange(this.href);
63
        }
64
    }
65
66
    handleClick(e) {
67
68
        const { onClick } = this.props;
69
70
        if (typeof onClick === 'function') {
71
72
            const onClickResult = onClick(e);
73
74
            if (typeof onClickResult === 'object' && typeof onClickResult.then === 'function') {
75
                e.persist();
76
                return onClickResult.then(() => {
77
                    this.initiateLocationChange(e);
78
                });
79
            }
80
        }
81
82
        return this.initiateLocationChange(e);
83
    }
84
85
    getHref(props) {
86
        let { to } = props;
87
88
        if (typeof to === 'object' && to.id) {
89
            to = this.router.parseRoute(to);
90
        }
91
92
        if (typeof to === 'string') {
93
            to = parse(to);
94
            to.query = qs.parse(to.query);
95
        }
96
97
        to.hash = typeof to.hash === 'string' && to.hash[0] !== '#' ? '#' + to.hash : to.hash;
98
99
        return to || false;
100
    }
101
102
    handleStoreChange() {
103
104
        if (!this.isSubscribed) return;
105
106
        const { activeClass, activeMatch } = this.props;
107
        const { pathname, hash, query, protocol } = this.href;
108
109
        if (!activeClass || !activeMatch || protocol) return; // eslint-disable-line consistent-return
110
111
        const routerStore = this.getStatefromStore();
112
        const { immutable } = this.router;
113
114
        let isActive = true;
115
116
        if (activeMatch instanceof RegExp) {
117
            const routePath = ( immutable ? routerStore.get('path') : routerStore.path );
118
119
            return this.setState({ // eslint-disable-line consistent-return
120
                isActive: activeMatch.test(routePath)
121
            });
122
        }
123
124
        if (activeMatch === LINK_MATCH_EXACT) {
125
            if (hash) {
126
                const routeHash = ( immutable ? routerStore.get('hash') : routerStore.hash );
127
                isActive = isActive && hash === routeHash;
128
            }
129
130
            if (query && Object.keys(query).length) {
131
                const routeQuery = immutable ? routerStore.get('query').toJS() : routerStore.query;
132
                isActive = isActive && Object.keys(query).reduce(
133
                        (result, item) => result && compareQueryItems(query[item], routeQuery[item]), true);
134
            }
135
        }
136
137
        const routePathname = ( immutable ? routerStore.get('pathname') : routerStore.pathname );
138
139
        isActive = isActive && (
140
            activeMatch === LINK_MATCH_EXACT
141
                ? pathname === routePathname
142
                : routePathname.indexOf(pathname) === 0
143
        );
144
145
        if (isActive !== this.state.isActive) {
146
            this.setState({
147
                isActive
148
            });
149
        }
150
    };
151
152
    locationChange(to) {
153
154
        const { method } = this.props;
155
156
        let search = to.query || to.search;
157
        search = typeof search === 'object' ? qs.stringify(search) : search;
158
159
        const payload = {
160
            pathname: to.pathname,
161
            search: search,
162
            hash: to.hash
163
        };
164
165
        this.store.dispatch(actions[method](payload));
166
    }
167
168
    render() {
169
170
        const { children, activeClass, className, target = null } = this.props;
171
        const classes = this.state.isActive ? `${className} ${activeClass}` : className;
172
173
        const props = {
174
            ...this.props,
175
            target,
176
            href: format(this.href),
177
            className: classes
178
        };
179
180
        props.onClick = this.handleClick;
181
182
        return React.createElement('a', props, children);
183
    }
184
}
185
186
Link.contextTypes = {
187
    router: PropTypes.object,
188
    store: PropTypes.object
189
};
190
191
Link.defaultProps = {
192
    to: '',
193
    className: LINK_CLASSNAME,
194
    activeClass: LINK_ACTIVE_CLASSNAME,
195
    method: LINK_DEFAULT_METHOD,
196
    activeMatch: false
197
};
198
199
if (__DEV__) {
200
    Link.propTypes = {
201
        to: PropTypes.oneOfType([
202
            PropTypes.string,
203
            PropTypes.object
204
        ]),
205
        className: PropTypes.string,
206
        activeClass: PropTypes.string,
207
        onClick: PropTypes.oneOfType([
208
            PropTypes.instanceOf(Function),
209
            PropTypes.instanceOf(Promise)
210
        ]),
211
        target: PropTypes.string,
212
        method: PropTypes.string,
213
        children: PropTypes.any,
214
        activeMatch: (props, propName, componentName) => {
215
            if (
216
                ![false, LINK_MATCH_EXACT, LINK_MATCH_PARTIAL].includes(props[propName]) &&
217
                !(props[propName] instanceof RegExp)
218
            ) {
219
                return new Error(
220
                    'Invalid prop `' + propName + '` supplied to' +
221
                    ' `' + componentName + '`. ' +
222
                    `Should be one of [false, '${LINK_MATCH_EXACT}', '${LINK_MATCH_PARTIAL}'] or an instance of RegExp`
223
                );
224
            }
225
        }
226
    };
227
}
228
229
export default Link;
230